import 'source-map-support/register';

import { assert } from 'chai';
import { DocumentNode, FragmentDefinitionNode } from 'graphql';

import gql from './index';
const loader = require('../loader');

describe('gql', () => {
  it('parses queries', () => {
    assert.equal(gql`{ testQuery }`.kind, 'Document');
  });

  it('parses queries when called as a function', () => {
    assert.equal(gql('{ testQuery }').kind, 'Document');
  });

  it('parses queries with weird substitutions', () => {
    const obj = Object.create(null);
    assert.equal(gql`{ field(input: "${obj.missing}") }`.kind, 'Document');
    assert.equal(gql`{ field(input: "${null}") }`.kind, 'Document');
    assert.equal(gql`{ field(input: "${0}") }`.kind, 'Document');
  });

  it('allows interpolation of documents generated by the webpack loader', () => {
    const sameFragment = "fragment SomeFragmentName on SomeType { someField }";

    const jsSource = loader.call(
      { cacheable() {} },
      sameFragment,
    );
    const module = { exports: Object.create(null) };

    Function("module", jsSource)(module);

    const document = gql`query { ...SomeFragmentName } ${module.exports}`;
    assert.equal(document.kind, 'Document');
    assert.equal(document.definitions.length, 2);
    assert.equal(document.definitions[0].kind, 'OperationDefinition');
    assert.equal(document.definitions[1].kind, 'FragmentDefinition');
  });

  it('parses queries through webpack loader', () => {
    const jsSource = loader.call({ cacheable() {} }, '{ testQuery }');
    const module = { exports: Object.create(null) };
    Function("module", jsSource)(module);
    assert.equal(module.exports.kind, 'Document');
  });

  it('parses single query through webpack loader', () => {
    const jsSource = loader.call({ cacheable() {} }, `
      query Q1 { testQuery }
    `);
    const module = { exports: Object.create(null) };
    Function("module", jsSource)(module);

    assert.equal(module.exports.kind, 'Document');
    assert.exists(module.exports.Q1);
    assert.equal(module.exports.Q1.kind, 'Document');
    assert.equal(module.exports.Q1.definitions.length, 1);
  });

  it('parses single query and exports as default', () => {
    const jsSource = loader.call({ cacheable() {} }, `
      query Q1 { testQuery }
    `);
    const module = { exports: Object.create(null) };
    Function("module", jsSource)(module);
    assert.deepEqual(module.exports.definitions, module.exports.Q1.definitions);
  });

  it('parses multiple queries through webpack loader', () => {
    const jsSource = loader.call({ cacheable() {} }, `
      query Q1 { testQuery }
      query Q2 { testQuery2 }
    `);
    const module = { exports: Object.create(null) };
    Function("module", jsSource)(module);

    assert.exists(module.exports.Q1);
    assert.exists(module.exports.Q2);
    assert.equal(module.exports.Q1.kind, 'Document');
    assert.equal(module.exports.Q2.kind, 'Document');
    assert.equal(module.exports.Q1.definitions.length, 1);
    assert.equal(module.exports.Q2.definitions.length, 1);
  });

  it('parses fragments with variable definitions', () => {
    gql.enableExperimentalFragmentVariables();

    const parsed: any = gql`fragment A ($arg: String!) on Type { testQuery }`;
    assert.equal(parsed.kind, 'Document');
    assert.exists(parsed.definitions[0].variableDefinitions);

    gql.disableExperimentalFragmentVariables()
  });

  // see https://github.com/apollographql/graphql-tag/issues/168
  it('does not nest queries needlessly in named exports', () => {
    const jsSource = loader.call({ cacheable() {} }, `
      query Q1 { testQuery }
      query Q2 { testQuery2 }
      query Q3 { test Query3 }
    `);
    const module = { exports: Object.create(null) };
    Function("module", jsSource)(module);

    assert.notExists(module.exports.Q2.Q1);
    assert.notExists(module.exports.Q3.Q1);
    assert.notExists(module.exports.Q3.Q2);
  });

  it('tracks fragment dependencies from multiple queries through webpack loader', () => {
    const jsSource = loader.call({ cacheable() {} }, `
      fragment F1 on F { testQuery }
      fragment F2 on F { testQuery2 }
      fragment F3 on F { testQuery3 }
      query Q1 { ...F1 }
      query Q2 { ...F2 }
      query Q3 {
        ...F1
        ...F2
      }
    `);
    const module = { exports: Object.create(null) };
    Function("module", jsSource)(module);

    assert.exists(module.exports.Q1);
    assert.exists(module.exports.Q2);
    assert.exists(module.exports.Q3);
    const Q1 = module.exports.Q1.definitions;
    const Q2 = module.exports.Q2.definitions;
    const Q3 = module.exports.Q3.definitions;

    assert.equal(Q1.length, 2);
    assert.equal(Q1[0].name.value, 'Q1');
    assert.equal(Q1[1].name.value, 'F1');

    assert.equal(Q2.length, 2);
    assert.equal(Q2[0].name.value, 'Q2');
    assert.equal(Q2[1].name.value, 'F2');

    assert.equal(Q3.length, 3);
    assert.equal(Q3[0].name.value, 'Q3');
    assert.equal(Q3[1].name.value, 'F1');
    assert.equal(Q3[2].name.value, 'F2');

      const F1 = module.exports.F1.definitions;
      const F2 = module.exports.F2.definitions;
      const F3 = module.exports.F3.definitions;

      assert.equal(F1.length, 1);
      assert.equal(F1[0].name.value, 'F1');
      assert.equal(F2.length, 1);
      assert.equal(F2[0].name.value, 'F2');
      assert.equal(F3.length, 1);
      assert.equal(F3[0].name.value, 'F3');

    });

  it('tracks fragment dependencies across nested fragments', () => {
    const jsSource = loader.call({ cacheable() {} }, `
      fragment F11 on F { testQuery }
      fragment F22 on F {
        ...F11
        testQuery2
      }
      fragment F33 on F {
        ...F22
        testQuery3
      }

      query Q1 {
        ...F33
      }

      query Q2 {
        id
      }
    `);

    const module = { exports: Object.create(null) };
    Function("module", jsSource)(module);

    assert.exists(module.exports.Q1);
    assert.exists(module.exports.Q2);

    const Q1 = module.exports.Q1.definitions;
    const Q2 = module.exports.Q2.definitions;

    assert.equal(Q1.length, 4);
    assert.equal(Q1[0].name.value, 'Q1');
    assert.equal(Q1[1].name.value, 'F33');
    assert.equal(Q1[2].name.value, 'F22');
    assert.equal(Q1[3].name.value, 'F11');

      assert.equal(Q2.length, 1);

      const F11 = module.exports.F11.definitions;
      const F22 = module.exports.F22.definitions;
      const F33 = module.exports.F33.definitions;

      assert.equal(F11.length, 1);
      assert.equal(F11[0].name.value, 'F11');
      assert.equal(F22.length, 2);
      assert.equal(F22[0].name.value, 'F22');
      assert.equal(F22[1].name.value, 'F11');
      assert.equal(F33.length, 3);
      assert.equal(F33[0].name.value, 'F33');
      assert.equal(F33[1].name.value, 'F22');
      assert.equal(F33[2].name.value, 'F11');
    });

  it('correctly imports other files through the webpack loader', () => {
    const query = `#import "./fragment_definition.graphql"
      query {
        author {
          ...authorDetails
        }
      }`;
    const jsSource = loader.call({ cacheable() {} }, query);
    const module = { exports: Object.create(null) };
    const require = (path: string) => {
      assert.equal(path, './fragment_definition.graphql');
      return gql`
        fragment authorDetails on Author {
          firstName
          lastName
        }`;
    };
    Function("module,require", jsSource)(module, require);
    assert.equal(module.exports.kind, 'Document');
    const definitions = module.exports.definitions;
    assert.equal(definitions.length, 2);
    assert.equal(definitions[0].kind, 'OperationDefinition');
    assert.equal(definitions[1].kind, 'FragmentDefinition');
  });

  it('importing files also works with a space between `#` and `import`', () => {
    const query = `# import "./fragment_definition.graphql"
      query {
        author {
          ...authorDetails
        }
      }`;
    const jsSource = loader.call({ cacheable() {} }, query);
    const module = { exports: Object.create(null) };
    const require = (path: string) => {
      assert.equal(path, './fragment_definition.graphql');
      return gql`
        fragment authorDetails on Author {
          firstName
          lastName
        }`;
    };
    Function("module,require", jsSource)(module, require);
    assert.equal(module.exports.kind, 'Document');
    const definitions = module.exports.definitions;
    assert.equal(definitions.length, 2);
    assert.equal(definitions[0].kind, 'OperationDefinition');
    assert.equal(definitions[1].kind, 'FragmentDefinition');
  });

  it('tracks fragment dependencies across fragments loaded via the webpack loader', () => {
    const query = `#import "./fragment_definition.graphql"
      fragment F111 on F {
        ...F222
      }

      query Q1 {
        ...F111
      }

      query Q2 {
        a
      }
      `;
    const jsSource = loader.call({ cacheable() {} }, query);
    const module = { exports: Object.create(null) };
    const require = (path: string) => {
      assert.equal(path, './fragment_definition.graphql');
      return gql`
        fragment F222 on F {
          f1
          f2
        }`;
    };
    Function("module,require", jsSource)(module, require);

    assert.exists(module.exports.Q1);
    assert.exists(module.exports.Q2);

    const Q1 = module.exports.Q1.definitions;
    const Q2 = module.exports.Q2.definitions;

    assert.equal(Q1.length, 3);
    assert.equal(Q1[0].name.value, 'Q1');
    assert.equal(Q1[1].name.value, 'F111');
    assert.equal(Q1[2].name.value, 'F222');

    assert.equal(Q2.length, 1);
  });

  it('does not complain when presented with normal comments', (done) => {
    assert.doesNotThrow(() => {
      const query = `#normal comment
        query {
          author {
            ...authorDetails
          }
        }`;
      const jsSource = loader.call({ cacheable() {} }, query);
      const module = { exports: Object.create(null) };
      Function("module", jsSource)(module);
      assert.equal(module.exports.kind, 'Document');
      done();
    });
  });

  it('returns the same object for the same query', () => {
    assert.isTrue(gql`{ sameQuery }` === gql`{ sameQuery }`);
  });

  it('returns the same object for the same query, even with whitespace differences', () => {
    assert.isTrue(gql`{ sameQuery }` === gql`  { sameQuery,   }`);
  });

  const fragmentAst = gql`
  fragment UserFragment on User {
    firstName
    lastName
  }
`;

  it('returns the same object for the same fragment', () => {
    assert.isTrue(gql`fragment same on Same { sameQuery }` ===
      gql`fragment same on Same { sameQuery }`);
  });

  it('returns the same object for the same document with substitution', () => {
    // We know that calling `gql` on a fragment string will always return
    // the same document, so we can reuse `fragmentAst`
    assert.isTrue(gql`{ ...UserFragment } ${fragmentAst}` ===
      gql`{ ...UserFragment } ${fragmentAst}`);
  });

  it('can reference a fragment that references as fragment', () => {
    const secondFragmentAst = gql`
      fragment SecondUserFragment on User {
        ...UserFragment
      }
      ${fragmentAst}
    `;

    const ast = gql`
      {
        user(id: 5) {
          ...SecondUserFragment
        }
      }
      ${secondFragmentAst}
    `;

    assert.deepEqual(ast, gql`
      {
        user(id: 5) {
          ...SecondUserFragment
        }
      }
      fragment SecondUserFragment on User {
        ...UserFragment
      }
      fragment UserFragment on User {
        firstName
        lastName
      }
    `);
  });

  describe('fragment warnings', () => {
    let warnings = [];
    const oldConsoleWarn = console.warn;
    beforeEach(() => {
      gql.resetCaches();
      warnings = [];
      console.warn = (w: string) => warnings.push(w);
    });
    afterEach(() => {
      console.warn = oldConsoleWarn;
    });

    it('warns if you use the same fragment name for different fragments', () => {
      const frag1 = gql`fragment TestSame on Bar { fieldOne }`;
      const frag2 = gql`fragment TestSame on Bar { fieldTwo }`;

      assert.isFalse(frag1 === frag2);
      assert.equal(warnings.length, 1);
    });

    it('does not warn if you use the same fragment name for the same fragment', () => {
      const frag1 = gql`fragment TestDifferent on Bar { fieldOne }`;
      const frag2 = gql`fragment TestDifferent on Bar { fieldOne }`;

      assert.isTrue(frag1 === frag2);
      assert.equal(warnings.length, 0);
    });

    it('does not warn if you use the same embedded fragment in two different queries', () => {
      const frag1 = gql`fragment TestEmbedded on Bar { field }`;
      const query1 = gql`{ bar { fieldOne ...TestEmbedded } } ${frag1}`;
      const query2 = gql`{ bar { fieldTwo ...TestEmbedded } } ${frag1}`;

      assert.isFalse(query1 === query2);
      assert.equal(warnings.length, 0);
    });

    it('does not warn if you use the same fragment name for embedded and non-embedded fragments', () => {
      const frag1 = gql`fragment TestEmbeddedTwo on Bar { field }`;
      gql`{ bar { ...TestEmbedded } } ${frag1}`;
      gql`{ bar { ...TestEmbedded } } fragment TestEmbeddedTwo on Bar { field }`;

      assert.equal(warnings.length, 0);
    });
  });

  describe('unique fragments', () => {
    beforeEach(() => {
      gql.resetCaches();
    });

    it('strips duplicate fragments from the document', () => {
      const frag1 = gql`fragment TestDuplicate on Bar { field }`;
      const query1 = gql`{ bar { fieldOne ...TestDuplicate } } ${frag1} ${frag1}`;
      const query2 = gql`{ bar { fieldOne ...TestDuplicate } } ${frag1}`;

      assert.equal(query1.definitions.length, 2);
      assert.equal(query1.definitions[1].kind, 'FragmentDefinition');
      // We don't test strict equality between the two queries because the source.body parsed from the
      // document is not the same, but the set of definitions should be.
      assert.deepEqual(query1.definitions, query2.definitions);
    });

    it('ignores duplicate fragments from second-level imports when using the webpack loader', () => {
      // take a require function and a query string, use the webpack loader to process it
      const load = (
        require: (path: string) => DocumentNode | null,
        query: string,
      ): DocumentNode | null => {
        const jsSource = loader.call({ cacheable() {} }, query);
        const module = { exports: Object.create(null) };
        Function("require,module", jsSource)(require, module);
        return module.exports;
      }

      const test_require = (path: string) => {
        switch (path) {
        case './friends.graphql':
          return load(test_require, [
            '#import "./person.graphql"',
            'fragment friends on Hero { friends { ...person } }',
          ].join('\n'));
        case './enemies.graphql':
          return load(test_require, [
            '#import "./person.graphql"',
            'fragment enemies on Hero { enemies { ...person } }',
          ].join('\n'));
        case './person.graphql':
          return load(test_require, 'fragment person on Person { name }\n');
        default:
          return null;
        };
      };

      const result = load(test_require, [
        '#import "./friends.graphql"',
        '#import "./enemies.graphql"',
        'query { hero { ...friends ...enemies } }',
      ].join('\n'))!;

      assert.equal(result.kind, 'Document');
      assert.equal(result.definitions.length, 4, 'after deduplication, only 4 fragments should remain');
      assert.equal(result.definitions[0].kind, 'OperationDefinition');

      // the rest of the definitions should be fragments and contain one of
      // each: "friends", "enemies", "person". Order does not matter
      const fragments = result.definitions.slice(1) as FragmentDefinitionNode[];
      assert(fragments.every(fragment => fragment.kind === 'FragmentDefinition'))
      assert(fragments.some(fragment => fragment.name.value === 'friends'))
      assert(fragments.some(fragment => fragment.name.value === 'enemies'))
      assert(fragments.some(fragment => fragment.name.value === 'person'))
    });
  });

  // How to make this work?
  // it.only('can reference a fragment passed as a document via shorthand', () => {
  //   const ast = gql`
  //     {
  //       user(id: 5) {
  //         ...${userFragmentDocument}
  //       }
  //     }
  //   `;
  //
  //   assert.deepEqual(ast, gql`
  //     {
  //       user(id: 5) {
  //         ...UserFragment
  //       }
  //     }
  //     fragment UserFragment on User {
  //       firstName
  //       lastName
  //     }
  //   `);
  // });
});
